diff options
Diffstat (limited to 'app/assets/javascripts/notes/components')
10 files changed, 152 insertions, 83 deletions
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 08d7c745791..79d8ce78329 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -84,6 +84,7 @@ export default { 'getNoteableDataByProp', 'getNotesData', 'openState', + 'hasDrafts', ]), ...mapState(['isToggleStateButtonLoading']), isNoteTypeComment() { @@ -171,6 +172,9 @@ export default { endpoint() { return this.getNoteableData.create_note_path; }, + draftEndpoint() { + return this.getNotesData.draftsPath; + }, issuableTypeTitle() { return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE ? this.$options.i18n.mergeRequest @@ -214,12 +218,15 @@ export default { this.errors = [this.$options.i18n.GENERIC_UNSUBMITTABLE_NETWORK]; } }, - handleSave(withIssueAction) { + handleSaveDraft() { + this.handleSave({ isDraft: true }); + }, + handleSave({ withIssueAction = false, isDraft = false } = {}) { this.errors = []; if (this.note.length) { const noteData = { - endpoint: this.endpoint, + endpoint: isDraft ? this.draftEndpoint : this.endpoint, data: { note: { noteable_type: this.noteableType, @@ -229,6 +236,7 @@ export default { }, merge_request_diff_head_sha: this.getNoteableData.diff_head_sha, }, + isDraft, }; if (this.noteType === constants.DISCUSSION) { @@ -392,62 +400,82 @@ export default { </markdown-field> </comment-field-layout> <div class="note-form-actions"> - <gl-form-checkbox - v-if="confidentialNotesEnabled && canSetConfidential" - v-model="noteIsConfidential" - class="gl-mb-6" - data-testid="confidential-note-checkbox" - > - {{ $options.i18n.confidential }} - <gl-icon - v-gl-tooltip:tooltipcontainer.bottom - name="question" - :size="16" - :title="$options.i18n.confidentialVisibility" - class="gl-text-gray-500" - /> - </gl-form-checkbox> - <gl-dropdown - split - :text="commentButtonTitle" - class="gl-mr-3 js-comment-button js-comment-submit-button comment-type-dropdown" - category="primary" - variant="success" - :disabled="disableSubmitButton" - data-testid="comment-button" - data-qa-selector="comment_button" - :data-track-label="trackingLabel" - data-track-event="click_button" - @click="handleSave()" - > - <gl-dropdown-item - is-check-item - :is-checked="isNoteTypeComment" - :selected="isNoteTypeComment" - @click="setNoteTypeToComment" + <template v-if="hasDrafts"> + <gl-button + :disabled="disableSubmitButton" + data-testid="add-to-review-button" + type="submit" + category="primary" + variant="success" + @click.prevent="handleSaveDraft()" + >{{ __('Add to review') }}</gl-button + > + <gl-button + :disabled="disableSubmitButton" + data-testid="add-comment-now-button" + category="secondary" + @click.prevent="handleSave()" + >{{ __('Add comment now') }}</gl-button + > + </template> + <template v-else> + <gl-form-checkbox + v-if="confidentialNotesEnabled && canSetConfidential" + v-model="noteIsConfidential" + class="gl-mb-6" + data-testid="confidential-note-checkbox" > - <strong>{{ $options.i18n.submitButton.comment }}</strong> - <p class="gl-m-0">{{ commentDescription }}</p> - </gl-dropdown-item> - <gl-dropdown-divider /> - <gl-dropdown-item - is-check-item - :is-checked="isNoteTypeDiscussion" - :selected="isNoteTypeDiscussion" - data-qa-selector="discussion_menu_item" - @click="setNoteTypeToDiscussion" + {{ $options.i18n.confidential }} + <gl-icon + v-gl-tooltip:tooltipcontainer.bottom + name="question" + :size="16" + :title="$options.i18n.confidentialVisibility" + class="gl-text-gray-500" + /> + </gl-form-checkbox> + <gl-dropdown + split + :text="commentButtonTitle" + class="gl-mr-3 js-comment-button js-comment-submit-button comment-type-dropdown" + category="primary" + variant="confirm" + :disabled="disableSubmitButton" + data-testid="comment-button" + data-qa-selector="comment_button" + :data-track-label="trackingLabel" + data-track-event="click_button" + @click="handleSave()" > - <strong>{{ $options.i18n.submitButton.startThread }}</strong> - <p class="gl-m-0">{{ startDiscussionDescription }}</p> - </gl-dropdown-item> - </gl-dropdown> + <gl-dropdown-item + is-check-item + :is-checked="isNoteTypeComment" + :selected="isNoteTypeComment" + @click="setNoteTypeToComment" + > + <strong>{{ $options.i18n.submitButton.comment }}</strong> + <p class="gl-m-0">{{ commentDescription }}</p> + </gl-dropdown-item> + <gl-dropdown-divider /> + <gl-dropdown-item + is-check-item + :is-checked="isNoteTypeDiscussion" + :selected="isNoteTypeDiscussion" + data-qa-selector="discussion_menu_item" + @click="setNoteTypeToDiscussion" + > + <strong>{{ $options.i18n.submitButton.startThread }}</strong> + <p class="gl-m-0">{{ startDiscussionDescription }}</p> + </gl-dropdown-item> + </gl-dropdown> + </template> <gl-button v-if="canToggleIssueState" :loading="isToggleStateButtonLoading" :class="[actionButtonClassNames, 'btn-comment btn-comment-and-close']" :disabled="isSubmitting" data-testid="close-reopen-button" - @click="handleSave(true)" + @click="handleSave({ withIssueAction: true })" >{{ issueActionButtonTitle }}</gl-button > </div> diff --git a/app/assets/javascripts/notes/components/discussion_navigator.vue b/app/assets/javascripts/notes/components/discussion_navigator.vue index fa3c900c337..7e8bb75902b 100644 --- a/app/assets/javascripts/notes/components/discussion_navigator.vue +++ b/app/assets/javascripts/notes/components/discussion_navigator.vue @@ -1,6 +1,11 @@ <script> /* global Mousetrap */ import 'mousetrap'; +import { + keysFor, + MR_NEXT_UNRESOLVED_DISCUSSION, + MR_PREVIOUS_UNRESOLVED_DISCUSSION, +} from '~/behaviors/shortcuts/keybindings'; import eventHub from '~/notes/event_hub'; import discussionNavigation from '~/notes/mixins/discussion_navigation'; @@ -10,12 +15,12 @@ export default { eventHub.$on('jumpToFirstUnresolvedDiscussion', this.jumpToFirstUnresolvedDiscussion); }, mounted() { - Mousetrap.bind('n', this.jumpToNextDiscussion); - Mousetrap.bind('p', this.jumpToPreviousDiscussion); + Mousetrap.bind(keysFor(MR_NEXT_UNRESOLVED_DISCUSSION), this.jumpToNextDiscussion); + Mousetrap.bind(keysFor(MR_PREVIOUS_UNRESOLVED_DISCUSSION), this.jumpToPreviousDiscussion); }, beforeDestroy() { - Mousetrap.unbind('n'); - Mousetrap.unbind('p'); + Mousetrap.unbind(keysFor(MR_NEXT_UNRESOLVED_DISCUSSION)); + Mousetrap.unbind(keysFor(MR_PREVIOUS_UNRESOLVED_DISCUSSION)); eventHub.$off('jumpToFirstUnresolvedDiscussion', this.jumpToFirstUnresolvedDiscussion); }, diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue index 0f74d78c8e0..dfe2763d8bd 100644 --- a/app/assets/javascripts/notes/components/discussion_notes.vue +++ b/app/assets/javascripts/notes/components/discussion_notes.vue @@ -121,6 +121,7 @@ export default { :is="componentName(firstNote)" :note="componentData(firstNote)" :line="line || diffLine" + :discussion-file="discussion.diff_file" :commit="commit" :help-page-path="helpPagePath" :show-reply-button="userCanReply" @@ -167,6 +168,7 @@ export default { v-for="(note, index) in discussion.notes" :key="note.id" :note="componentData(note)" + :discussion-file="discussion.diff_file" :help-page-path="helpPagePath" :line="diffLine" :discussion-root="index === 0" diff --git a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue index cace382ccd6..5f429cbf462 100644 --- a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue +++ b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue @@ -1,7 +1,11 @@ <script> import { GlTooltipDirective, GlButton } from '@gitlab/ui'; +import { s__ } from '~/locale'; export default { + i18n: { + buttonLabel: s__('MergeRequests|Resolve this thread in a new issue'), + }, name: 'ResolveWithIssueButton', components: { GlButton, @@ -23,7 +27,8 @@ export default { <gl-button v-gl-tooltip :href="url" - :title="s__('MergeRequests|Resolve this thread in a new issue')" + :title="$options.i18n.buttonLabel" + :aria-label="$options.i18n.buttonLabel" class="new-issue-for-discussion discussion-create-issue-btn" icon="issue-new" /> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index ed6701b34e8..24399e669a6 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -13,6 +13,12 @@ import { splitCamelCase } from '../../lib/utils/text_utility'; import ReplyButton from './note_actions/reply_button.vue'; export default { + i18n: { + addReactionLabel: __('Add reaction'), + editCommentLabel: __('Edit comment'), + deleteCommentLabel: __('Delete comment'), + moreActionsLabel: __('More actions'), + }, name: 'NoteActions', components: { GlIcon, @@ -119,9 +125,11 @@ export default { type: Boolean, required: true, }, + // This can be undefined when `canAwardEmoji` is false awardPath: { type: String, - required: true, + required: false, + default: '', }, }, computed: { @@ -301,9 +309,9 @@ export default { category="tertiary" variant="default" size="small" - title="Add reaction" + :title="$options.i18n.addReactionLabel" + :aria-label="$options.i18n.addReactionLabel" data-position="right" - :aria-label="__('Add reaction')" > <span class="reaction-control-icon reaction-control-icon-neutral"> <gl-icon name="slight-smile" /> @@ -325,32 +333,35 @@ export default { <gl-button v-if="canEdit" v-gl-tooltip - title="Edit comment" + :title="$options.i18n.editCommentLabel" + :aria-label="$options.i18n.editCommentLabel" icon="pencil" size="small" category="tertiary" - class="note-action-button js-note-edit btn btn-transparent" + class="note-action-button js-note-edit" data-qa-selector="note_edit_button" @click="onEdit" /> <gl-button v-if="showDeleteAction" v-gl-tooltip - title="Delete comment" + :title="$options.i18n.deleteCommentLabel" + :aria-label="$options.i18n.deleteCommentLabel" size="small" icon="remove" category="tertiary" - class="note-action-button js-note-delete btn btn-transparent" + class="note-action-button js-note-delete" @click="onDelete" /> <div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions"> <gl-button v-gl-tooltip - title="More actions" + :title="$options.i18n.moreActionsLabel" + :aria-label="$options.i18n.moreActionsLabel" icon="ellipsis_v" size="small" category="tertiary" - class="note-action-button more-actions-toggle btn btn-transparent" + class="note-action-button more-actions-toggle" data-toggle="dropdown" @click="closeTooltip" /> diff --git a/app/assets/javascripts/notes/components/note_attachment.vue b/app/assets/javascripts/notes/components/note_attachment.vue index b20facc4032..c49f3e2de99 100644 --- a/app/assets/javascripts/notes/components/note_attachment.vue +++ b/app/assets/javascripts/notes/components/note_attachment.vue @@ -24,7 +24,7 @@ export default { target="_blank" rel="noopener noreferrer" > - <img :src="attachment.url" class="note-image-attach" /> + <img :src="attachment.url" class="note-image-attach col-lg-4" /> </a> <div class="attachment"> <a diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index d74ade15de1..a70bac94b71 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -60,6 +60,11 @@ export default { required: false, default: null, }, + lines: { + type: Array, + required: false, + default: () => [], + }, note: { type: Object, required: false, @@ -333,6 +338,7 @@ export default { :help-page-path="helpPagePath" :show-suggest-popover="showSuggestPopover" :textarea-value="updatedNoteBody" + :lines="lines" @handleSuggestDismissed="() => $emit('handleSuggestDismissed')" > <template #textarea> @@ -384,7 +390,7 @@ export default { <gl-button :disabled="isDisabled" category="primary" - variant="success" + variant="confirm" class="gl-mr-3" data-qa-selector="start_review_button" @click="handleAddToReview" @@ -418,7 +424,7 @@ export default { <gl-button :disabled="isDisabled" category="primary" - variant="success" + variant="confirm" data-qa-selector="reply_comment_button" class="gl-mr-3 js-vue-issue-save js-comment-button" @click="handleUpdate()" diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 185f4a70367..0feb77be653 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -48,6 +48,11 @@ export default { required: false, default: null, }, + discussionFile: { + type: Object, + required: false, + default: null, + }, helpPagePath: { type: String, required: false, @@ -86,7 +91,7 @@ export default { isRequesting: false, isResolving: false, commentLineStart: {}, - resolveAsThread: this.glFeatures.removeResolveNote, + resolveAsThread: true, }; }, computed: { @@ -139,14 +144,9 @@ export default { return this.note.isDraft; }, canResolve() { - if (this.glFeatures.removeResolveNote && !this.discussionRoot) return false; + if (!this.discussionRoot) return false; - if (this.glFeatures.removeResolveNote) return this.note.current_user.can_resolve_discussion; - - return ( - this.note.current_user.can_resolve || - (this.note.isDraft && this.note.discussion_id !== null) - ); + return this.note.current_user.can_resolve_discussion; }, lineRange() { return this.note.position?.line_range; @@ -172,12 +172,18 @@ export default { return commentLineOptions(lines, this.commentLineStart, this.line.line_code); }, diffFile() { + let fileResolvedFromAvailableSource; + if (this.commentLineStart.line_code) { const lineCode = this.commentLineStart.line_code.split('_')[0]; - return this.getDiffFileByHash(lineCode); + fileResolvedFromAvailableSource = this.getDiffFileByHash(lineCode); + } + + if (!fileResolvedFromAvailableSource && this.discussionFile) { + fileResolvedFromAvailableSource = this.discussionFile; } - return null; + return fileResolvedFromAvailableSource || null; }, }, created() { diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 58cfd150659..433f75a752d 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -3,8 +3,10 @@ import { mapGetters, mapActions } from 'vuex'; import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user'; import { __ } from '~/locale'; import initUserPopovers from '~/user_popovers'; +import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import OrderedLayout from '~/vue_shared/components/ordered_layout.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import draftNote from '../../batch_comments/components/draft_note.vue'; import { deprecatedCreateFlash as Flash } from '../../flash'; import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; @@ -32,6 +34,8 @@ export default { discussionFilterNote, OrderedLayout, SidebarSubscription, + draftNote, + TimelineEntryItem, }, mixins: [glFeatureFlagsMixin()], props: { @@ -276,6 +280,9 @@ export default { <ul id="notes-list" class="notes main-notes-list timeline"> <template v-for="discussion in allDiscussions"> <skeleton-loading-container v-if="discussion.isSkeletonNote" :key="discussion.id" /> + <timeline-entry-item v-else-if="discussion.isDraft" :key="discussion.id"> + <draft-note :draft="discussion" /> + </timeline-entry-item> <template v-else-if="discussion.isPlaceholderNote"> <placeholder-system-note v-if="discussion.placeholderType === $options.systemNote" diff --git a/app/assets/javascripts/notes/components/sort_discussion.vue b/app/assets/javascripts/notes/components/sort_discussion.vue index ed1f456c174..92c39fbb9f0 100644 --- a/app/assets/javascripts/notes/components/sort_discussion.vue +++ b/app/assets/javascripts/notes/components/sort_discussion.vue @@ -49,18 +49,17 @@ export default { </script> <template> - <div class="gl-mr-3 gl-display-inline-block gl-vertical-align-bottom full-width-mobile"> + <div + data-testid="sort-discussion-filter" + class="gl-mr-3 gl-display-inline-block gl-vertical-align-bottom full-width-mobile" + > <local-storage-sync :value="sortDirection" :storage-key="storageKey" :persist="persistSortOrder" @input="setDiscussionSortDirection({ direction: $event })" /> - <gl-dropdown - :text="dropdownText" - data-testid="sort-discussion-filter" - class="js-dropdown-text full-width-mobile" - > + <gl-dropdown :text="dropdownText" class="js-dropdown-text full-width-mobile"> <gl-dropdown-item v-for="{ text, key, cls } in $options.SORT_OPTIONS" :key="key" |